/*  Starshatter OpenSource Distribution
    Copyright (c) 1997-2004, Destroyer Studios LLC.
    All Rights Reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice,
      this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice,
      this list of conditions and the following disclaimer in the documentation
      and/or other materials provided with the distribution.
    * Neither the name "Destroyer Studios" nor the names of its contributors
      may be used to endorse or promote products derived from this software
      without specific prior written permission.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    POSSIBILITY OF SUCH DAMAGE.

    SUBSYSTEM:    nGenEx.lib
    FILE:         SoundD3D.cpp
    AUTHOR:       John DiCamillo


    OVERVIEW
    ========
    DirectSound and DirectSound3D (sound card) class
*/

//#define INITGUID
#include <objbase.h>
#include <cguid.h>
#include <mmsystem.h>
#include <dsound.h>


#include "MemDebug.h"
#include "SoundD3D.h"
#include "Game.h"

#ifdef DIRECT_SOUND_3D
#include "ia3d.h"
#endif

// +--------------------------------------------------------------------+

void  Print(const char* msg, ...);
char* DSErrStr(HRESULT dserr);
void  SoundD3DError(const char* msg, HRESULT dserr);

static int           DS3D_report_errors = 1;

#ifndef RELEASE
#define RELEASE(x) if (x) { x->Release(); x=NULL; }
#endif

// +====================================================================+
// | SOUND CARD D3D
// +====================================================================+

SoundCardD3D::SoundCardD3D(HWND hwnd)
: soundcard(0), primary(0)
{
    HRESULT  err = 0;
    status = SC_ERROR;

    // 1) Get interface to DirectSound object:

#ifdef DIRECT_SOUND_3D
    CoInitialize(NULL);
    err = CoCreateInstance(CLSID_A3d, NULL, CLSCTX_INPROC_SERVER,  
    IID_IDirectSound, (VOID **)&soundcard);

    if (SUCCEEDED(err)) {
        soundcard->Initialize(NULL);
        SoundD3DError("Initialized Aureal3D Sound", 0);
    }
    else {
        SoundD3DError("Could not initialize Aureal3D Sound", err);
        SoundD3DError("Proceding with standard DirectSoundCreate", 0);
#endif      

        err = DirectSoundCreate(0, &soundcard, 0);
        if (FAILED(err)) {
            SoundD3DError("Could not create DirectSound object.", err);
            soundcard = 0;
            primary   = 0;
            return;
        }

#ifdef DIRECT_SOUND_3D
    }
#endif

    // 2) Set the cooperative level:
    err = soundcard->SetCooperativeLevel(hwnd, DSSCL_PRIORITY);
    if (FAILED(err)) {
        SoundD3DError("Could not set cooperative level.", err);
        RELEASE(soundcard);
        return;
    }

    // Prepare to initialize the primary buffer:
    DSCAPS caps;
    memset(&caps, 0, sizeof(caps));
    caps.dwSize    = sizeof(caps);

    err = soundcard->GetCaps(&caps);
    if (FAILED(err)) {
        SoundD3DError("Could not get soundcard caps.", err);
        RELEASE(soundcard);
        return;
    }

    if (caps.dwFlags & DSCAPS_EMULDRIVER)
    Print("   WARNING: using DirectSound emulated drivers\n");

    memset(&dsbd, 0, sizeof(dsbd));
    dsbd.dwSize    = sizeof(dsbd);

#ifdef DIRECT_SOUND_3D
    int use_ds3d = true;

    // 3) Set up the primary buffer (try to use DS3D):
    dsbd.dwFlags   = DSBCAPS_PRIMARYBUFFER | DSBCAPS_CTRL3D;

    err = soundcard->CreateSoundBuffer(&dsbd, &primary, 0);
    if (err == DSERR_CONTROLUNAVAIL) {
        use_ds3d = false;

        // try again, without using DS3D
        dsbd.dwFlags   = DSBCAPS_PRIMARYBUFFER | DSBCAPS_CTRLDEFAULT;

        err = soundcard->CreateSoundBuffer(&dsbd, &primary, 0);
        if (FAILED(err)) {
            SoundD3DError("Could not initialize primary buffer", err);
            RELEASE(soundcard);
            return;
        }
        else {
            Print("   WARNING: DirectSound3D is not available, simulating instead\n");
        }
    }

    // 4) Set up the listener:
    if (primary && use_ds3d) {
        err = primary->QueryInterface(IID_IDirectSound3DListener, (void**)&listener);
        if (FAILED(err)) {
            SoundD3DError("Could not get listener interface", err);
        }
        else {
            listener->SetPosition(0.0f, 0.0f, 0.0f, DS3D_IMMEDIATE);
            listener->SetOrientation(0.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, DS3D_IMMEDIATE);
        }
    }

#else
    // 3) Set up the primary buffer:
    dsbd.dwFlags   = DSBCAPS_PRIMARYBUFFER;

    err = soundcard->CreateSoundBuffer(&dsbd, &primary, 0);
    if (FAILED(err)) {
        SoundD3DError("Could not initialize primary buffer", err);
        RELEASE(soundcard);
        return;
    }
#endif

    // 5) Set primary buffer parameters to 16 bit STEREO at 44kHz
    SetFormat(16, 2, 44100);

    // read back the result format and display to the log file:
    GetFormat(0);
    ShowFormat();

    status = SC_OK;
}

// +--------------------------------------------------------------------+

SoundCardD3D::~SoundCardD3D()
{
    ListIter<Sound> iter = sounds;
    while (++iter) {
        Sound* s = iter.value();
        s->Stop();
    }

    sounds.destroy();

#ifdef DIRECT_SOUND_3D
    RELEASE(listener);
#endif

    RELEASE(primary);
    RELEASE(soundcard);

    Print("   SoundCardD3D: shutdown\n");

    status = SC_UNINITIALIZED;
}

// +--------------------------------------------------------------------+

bool
SoundCardD3D::SetFormat(int bits, int channels, int hertz)
{
    if (!soundcard) return false;

    DSCAPS caps;
    memset(&caps, 0, sizeof(caps));
    caps.dwSize    = sizeof(caps);
    soundcard->GetCaps(&caps);

    if (!(caps.dwFlags & DSCAPS_PRIMARY16BIT))  bits     = 8;
    if (!(caps.dwFlags & DSCAPS_PRIMARYSTEREO)) channels = 1;

    memset(&wfex, 0, sizeof(wfex));

    wfex.wFormatTag      = WAVE_FORMAT_PCM;
    wfex.nChannels       = channels;
    wfex.nSamplesPerSec  = hertz;
    wfex.nBlockAlign     = (channels * bits) / 8;
    wfex.nAvgBytesPerSec = wfex.nSamplesPerSec * wfex.nBlockAlign;
    wfex.wBitsPerSample  = bits;

    return SetFormat(&wfex);
}

// +--------------------------------------------------------------------+

bool
SoundCardD3D::SetFormat(LPWAVEFORMATEX format)
{
    HRESULT err = E_FAIL;

    if (primary)
    err = primary->SetFormat(format);

    return SUCCEEDED(err);
}

// +--------------------------------------------------------------------+

bool
SoundCardD3D::GetFormat(LPWAVEFORMATEX format)
{
    if (!format) format = &wfex;

    HRESULT err = E_FAIL;

    if (primary)
    err = primary->GetFormat(format, sizeof(WAVEFORMATEX), 0);

    return SUCCEEDED(err);
}

// +--------------------------------------------------------------------+

void
SoundCardD3D::ShowFormat()
{
    Print("   SoundCardD3D Primary Buffer Format:\n");
    Print("     bits: %d\n",   wfex.wBitsPerSample);
    Print("     chls: %d\n",   wfex.nChannels);
    Print("     rate: %d\n\n", wfex.nSamplesPerSec);
}

// +--------------------------------------------------------------------+

Sound*
SoundCardD3D::CreateSound(DWORD flags, LPWAVEFORMATEX format)
{
    if (!soundcard) return 0;

    Sound* result = new(__FILE__,__LINE__) SoundD3D(soundcard, flags, format);
    if (result) AddSound(result);
    return result;
}

// +--------------------------------------------------------------------+

Sound*
SoundCardD3D::CreateSound(DWORD flags, LPWAVEFORMATEX format, DWORD len, LPBYTE data)
{
    if (!soundcard) return 0;
    Sound* result = new(__FILE__,__LINE__) SoundD3D(soundcard, flags, format, len, data);

    if (flags & (Sound::STREAMED | Sound::OGGVORBIS)) {
        if (result)
        AddSound(result);
    }

    return result;
}

// +--------------------------------------------------------------------+

void
SoundCardD3D::SetListener(const Camera& cam, const Vec3& vel)
{
    Point pos = cam.Pos();

#ifdef DIRECT_SOUND_3D
    listener->SetPosition((float) pos.x, (float) pos.z, (float) pos.y, DS3D_IMMEDIATE);
    listener->SetOrientation((float) cam.vpn().x, (float) cam.vpn().y, (float) cam.vpn().z,
    (float) cam.vup().x, (float) cam.vup().y, (float) cam.vup().z,
    DS3D_IMMEDIATE);
    listener->SetVelocity(vel.x, vel.y, vel.z, DS3D_IMMEDIATE);
#else
    listener.Clone(cam);
    listener.MoveTo(pos.x, pos.z, pos.y);
    velocity = vel;
#endif
}

// +--------------------------------------------------------------------+

bool
SoundCardD3D::Pause()
{
    AutoThreadSync a(sync);

    ListIter<Sound> iter = sounds;
    while (++iter) {
        Sound* s = iter.value();

        if ((s->GetFlags() & Sound::INTERFACE) == 0)
        s->Pause();
    }

    return true;
}

// +--------------------------------------------------------------------+

bool
SoundCardD3D::Resume()
{
    AutoThreadSync a(sync);

    ListIter<Sound> iter = sounds;
    while (++iter) {
        Sound* s = iter.value();

        if (s->IsReady())
        s->Play();
    }

    return true;
}

// +--------------------------------------------------------------------+

bool
SoundCardD3D::StopSoundEffects()
{
    AutoThreadSync a(sync);

    DWORD ok_sounds = (Sound::INTERFACE | Sound::OGGVORBIS | Sound::RESOURCE);

    ListIter<Sound> iter = sounds;
    while (++iter) {
        Sound* s = iter.value();

        if ((s->GetFlags() & ok_sounds) == 0)
        s->Stop();
    }

    return true;
}

// +====================================================================+
// | SOUND D3D
// +====================================================================+

SoundD3D::SoundD3D(LPDIRECTSOUND card, DWORD flag_req, LPWAVEFORMATEX format)
: soundcard(card), buffer(0), min_dist(1.0f), max_dist(100000.0f),
stream(0), stream_left(0), min_safety(0), read_size(0), transfer(0), w(0), r(0),
stream_offset(0), data_len(0), data(0), moved(false), eos_written(false), eos_latch(0),
ov_file(0), total_time(0)
{
    flags = flag_req;

    CopyMemory(&wfex, format, sizeof(wfex));
    ZeroMemory(&dsbd, sizeof(dsbd));

    dsbd.dwSize  = sizeof(dsbd);
    dsbd.dwFlags = DSBCAPS_CTRLVOLUME /* | DSBCAPS_GETCURRENTPOSITION2 */;
    dsbd.lpwfxFormat = &wfex;

#ifdef DIRECT_SOUND_3D
    sound3d = 0;
    if (flags & LOCALIZED)
    if (flags & LOC_3D)
    dsbd.dwFlags |= DSBCAPS_CTRL3D;
    else
    dsbd.dwFlags |= DSBCAPS_CTRLPAN | DSBCAPS_CTRLFREQUENCY;
#else
    dsbd.dwFlags |= DSBCAPS_LOCSOFTWARE;

    if (flags & LOCALIZED)
    dsbd.dwFlags |= DSBCAPS_CTRLPAN | DSBCAPS_CTRLFREQUENCY;
#endif

}

// +--------------------------------------------------------------------+
// SOUND RESOURCE CONSTRUCTOR:
// (Now also used to create Ogg Vorbis streaming sound objects)

SoundD3D::SoundD3D(LPDIRECTSOUND card, DWORD flag_req, LPWAVEFORMATEX format, DWORD len, LPBYTE pData)
: soundcard(card), buffer(0), min_dist(1.0f), max_dist(100000.0f),
stream(0), stream_left(0), min_safety(0), read_size(0), transfer(0), w(0), r(0),
stream_offset(0), data_len(0), data(0), moved(false), eos_written(false), eos_latch(0),
ov_file(0)
{
    flags = flag_req;

    if (!(flags & (STREAMED | OGGVORBIS)))
    flags = flag_req | RESOURCE;

    CopyMemory(&wfex, format, sizeof(wfex));
    ZeroMemory(&dsbd, sizeof(dsbd));

    dsbd.dwSize  = sizeof(dsbd);
    dsbd.dwFlags = DSBCAPS_CTRLVOLUME /* | DSBCAPS_GETCURRENTPOSITION2 */;
    dsbd.lpwfxFormat = &wfex;

#ifdef DIRECT_SOUND_3D
    sound3d = 0;
    if (flags & LOCALIZED)
    if (flags & LOC_3D)
    dsbd.dwFlags |= DSBCAPS_CTRL3D;
    else
    dsbd.dwFlags |= DSBCAPS_CTRLPAN | DSBCAPS_CTRLFREQUENCY;
#else
    if (flags & LOCALIZED)
    dsbd.dwFlags |= DSBCAPS_CTRLPAN | DSBCAPS_CTRLFREQUENCY;
#endif

    if (len) {
        // If this is an OGG VORBIS streaming sound,
        // the parameter that normally points to the actual data
        // is used to pass in an Ogg Vorbis file structure instead:

        if (flags & OGGVORBIS) {
            ov_file = (OggVorbis_File*) pData;
            StreamOggFile();
        }

        else {
            data_len = len;
            data     = new(__FILE__,__LINE__) BYTE[len];

            if (!data) {
                data_len = 0;
            }

            else {
                CopyMemory(data, pData, data_len);
                Load(data_len, data);
            }
        }
    }
}

// +--------------------------------------------------------------------+

SoundD3D::~SoundD3D()
{
    delete [] data;
    delete [] transfer;

    if (ov_file) {
        ov_clear(ov_file);
        delete ov_file;
    }

    else if (stream) {
        fclose(stream);
    }

    RELEASE(buffer);

#ifdef DIRECT_SOUND_3D
    RELEASE(sound3d);
#endif
}

// +--------------------------------------------------------------------+

void
SoundD3D::Update()
{
    if (!buffer || status != PLAYING) return; // nothing to do

    DWORD dstat;
    HRESULT hr = buffer->GetStatus(&dstat);
    if (FAILED(hr)) {
        SoundD3DError("Update: GetStatus failed", hr);
        return;
    }

    AutoThreadSync a(sync);

    if (sound_check) sound_check->Update(this);

    if (!Game::Paused() || flags & STREAMED) {
        // see if we are done:
        if (!(dstat & DSBSTATUS_PLAYING)) {
            status = DONE;
            buffer->Stop();
            return;
        }

        // if not done, see if we need to change location params:
        if (moved) {
            Localize();
        }
    }

    // if not done, see if we need to load more data
    // into the streaming buffer:
    if (flags & STREAMED) {
        buffer->GetCurrentPosition(&r, 0);
        
        DWORD data_left;
        if (w > r)
        data_left = w - r;
        else
        data_left = w + (read_size + min_safety) - r;

        // getting low, fill 'er up:
        if (eos_written || data_left <= min_safety) {
            StreamBlock();

            if (stream_left == 0) {
                // if this is the end of a looping stream,
                if (flags & LOOP) {
                    RewindStream();
                    looped++;
                }
                else {
                    if (!eos_written) {
                        eos_written = true;
                        eos_latch   = 3;
                    }

                    else if (--eos_latch == 0) {
                        status = DONE;
                        buffer->Stop();
                    }
                }
            }

            status = PLAYING;
        }
    }
}

void
SoundD3D::StreamBlock()
{
    if (flags & OGGVORBIS) {
        StreamOggBlock();
        return;
    }

    if (!stream || !stream_left)
    return;

    if (stream_left < read_size) {
        if (stream_left > 0) {
            fread(transfer, stream_left, 1, stream);
            Load(stream_left, transfer);
            stream_left = 0;
        }
    }

    else if (read_size > 0) {
        fread(transfer, read_size, 1, stream);
        Load(read_size, transfer);
        stream_left -= read_size;
    }
}

int ogg_read_error_count = 0;

void
SoundD3D::StreamOggBlock()
{
    int   current_bitstream;
    DWORD bytes_read = 0;
    long  retval     = 0;
    char* p          = (char*) transfer;

    while (stream_left && bytes_read < read_size) {
        retval = ov_read(ov_file, p, read_size-bytes_read, 0,2,1, &current_bitstream);

        if (retval == 0) {
            /* EOF */
            stream_left = 0;
        }
        else if (retval < 0) {
            /* error in the stream.  Not a problem, just reporting it in
            case the app cares.  In this case, we don't. */
            ogg_read_error_count++;
        }
        else {
            /* we don't bother dealing with sample rate changes, etc, but you'll have to ??? */
            bytes_read  += retval;
            stream_left -= retval;
            p           += retval;
        }
    }

    if (bytes_read)
    Load(bytes_read, transfer);
}

void
SoundD3D::RewindStream()
{
    if (flags & OGGVORBIS) {
        RewindOggStream();
        return;
    }

    if (!stream || !buffer)
    return;

    // rewind the stream and keep going...
    eos_written = false;
    eos_latch   = 0;
    read_size   = wfex.nAvgBytesPerSec / 2;

    // find the size of the file:
    fseek(stream, 0, SEEK_END);
    stream_left = ftell(stream) - stream_offset;
    fseek(stream, stream_offset, SEEK_SET);

    total_time = (double) stream_left /
    (double) wfex.nAvgBytesPerSec;

    if (stream_left < read_size) {
        status = DONE;
        buffer->Stop();
    }
}

void
SoundD3D::RewindOggStream()
{
    if (!ov_file || !buffer)
    return;

    // rewind the stream and keep going...
    eos_written = false;
    eos_latch   = 0;
    read_size   = wfex.nAvgBytesPerSec / 2;

    // set the stream pointer back to the beginning:
    ov_pcm_seek(ov_file, 0);

    // find the size of the file:
    stream_left    = (DWORD) ov_pcm_total(ov_file,-1);
    stream_offset  = 0;

    total_time     = (double) stream_left /
    (double) wfex.nAvgBytesPerSec;

    if (stream_left < read_size) {
        status = DONE;
        buffer->Stop();
    }
}

void
SoundD3D::Localize()
{
#ifdef DIRECT_SOUND_3D
    if (sound3d) {
        sound3d->SetMinDistance(min_dist,                        DS3D_IMMEDIATE);
        sound3d->SetMaxDistance(max_dist,                        DS3D_IMMEDIATE);
        sound3d->SetPosition(location.x, location.y, location.z, DS3D_IMMEDIATE);
        sound3d->SetVelocity(velocity.x, velocity.y, velocity.z, DS3D_IMMEDIATE);
    }

#else

    // if no buffer, nothing to do:
    if (!buffer) {
        moved = false;
        return;
    }

    // Compute pan and volume from scratch:
    
    if ((flags & LOC_3D) && creator) {
        Vec3 loc = location;

        SoundCardD3D* ears = (SoundCardD3D*) creator;
        Camera& listener = ears->listener;
        Vec3  ear_loc = listener.Pos();  ear_loc.SwapYZ();
        Vec3  direction = loc - ear_loc;
        
        loc.x = direction * listener.vrt();
        loc.y = direction * listener.vup();
        loc.z = direction * listener.vpn();

        double pan = 10000;
        if (loc.z != 0.0f) pan = fabs(1000.0f * loc.x / loc.z);
        if (pan > 10000)   pan = 10000;
        if (loc.x < 0)     pan = -pan;

        if (volume > 0)
        volume = 0;

        double vol   = volume;
        double mind2 = min_dist * min_dist;
        double maxd2 = max_dist * max_dist;
        double d2    = (loc.x*loc.x) + (loc.y*loc.y) + (loc.z*loc.z);

        if (d2 > maxd2)
        vol = -10000;
        else if (d2 > mind2)
        vol -= (d2-mind2)/(maxd2-mind2) * (vol+10000);

        // clamp volume to legal range:
        if (vol < -10000) vol = -10000;
        else if (vol > volume) vol = volume;

        /***
    Print("Localize: ears  = (%f, %f, %f)\n", ear_loc.x, ear_loc.y, ear_loc.z);
    Print("          world = (%f, %f, %f)\n", location.x, location.y, location.z);
    Print("          view  = (%f, %f, %f)\n", loc.x, loc.y, loc.z);
    Print("          Pan=%f  Volume=%f\n", pan, vol);
    /***/

        HRESULT hr = buffer->SetPan((LONG) pan);
        if (!SUCCEEDED(hr)) {
            char warn[512];
            sprintf_s(warn, "Warning could not set pan on buffer to %f", pan);
            SoundD3DError(warn, hr);
        }

        hr = buffer->SetVolume((LONG) vol);
        if (!SUCCEEDED(hr)) {
            char warn[512];
            sprintf_s(warn, "Warning: could not set volume on buffer to %f", vol);
            SoundD3DError(warn, hr);
        }
        
        // if not too far to hear...
        if ((flags & DOPPLER) && (d2 < maxd2)) {
            // COMPUTE DOPPLER SHIFT:
            const float c = 10000.0f;
            
            direction.Normalize();
            float v_L = ears->velocity * direction;
            float v_S =       velocity * direction;
            
            DWORD f_shift = wfex.nSamplesPerSec;

            if (v_L != v_S) {
                // towards listener:
                if (v_S < 0)
                f_shift = wfex.nSamplesPerSec + 20;
                else
                f_shift = wfex.nSamplesPerSec - 20;
            }

            // distance rolloff of high frequencies:
            double dist     = sqrt(d2);
            DWORD  roll_off = (DWORD) (80 * dist/max_dist);

            f_shift -= roll_off;
            
            if (f_shift < 100)    f_shift = 100;
            if (f_shift > 100000) f_shift = 100000;
            
            hr = buffer->SetFrequency(f_shift);
            if (!SUCCEEDED(hr)) {
                char warn[512];
                sprintf_s(warn, "Warning: could not set Doppler frequency on buffer to %d", f_shift); //-V576
                SoundD3DError(warn, hr);
            }
        }
    }
    else {
        buffer->SetPan((LONG) location.x);
        buffer->SetVolume((LONG) volume);
    }
#endif

    moved = false;
}

// +--------------------------------------------------------------------+

Sound*
SoundD3D::Duplicate()
{
    Sound* sound = 0;

    if (flags & RESOURCE) {
        sound = Sound::Create(flags & ~RESOURCE, &wfex);

        if (sound && !(flags & STREAMED)) {
            sound->SetMinDistance(min_dist);
            sound->SetMaxDistance(max_dist);

            if (!buffer) {
                sound->Load(data_len, data);
            }

            else {
                SoundD3D* s3d = (SoundD3D*) sound;
                soundcard->DuplicateSoundBuffer(buffer, &s3d->buffer);
                sound->Rewind();
            }
        }
    }

    return sound;
}

// +--------------------------------------------------------------------+

HRESULT
SoundD3D::StreamFile(const char* name, DWORD offset)
{
    DWORD buf_size    = wfex.nAvgBytesPerSec / 2;
    DWORD safety_zone = buf_size * 2;

    if (stream) {
        delete[] transfer;
        transfer = 0;
        fclose(stream);
    }

    status         = UNINITIALIZED;
    stream_left    = 0;
    stream_offset  = offset;

    eos_written    = false;
    eos_latch      = 0;
    min_safety     = safety_zone;
    read_size      = buf_size;

    fopen_s(&stream, name, "rb");

    // open the stream:
    if (stream == 0) {
        SoundD3DError("StreamFile: could not open stream", E_FAIL);
        return E_FAIL;
    }

    // find the size of the file:
    fseek(stream, 0, SEEK_END);
    stream_left = ftell(stream) - offset;
    fseek(stream, stream_offset, SEEK_SET);

    total_time = (double) stream_left /
    (double) wfex.nAvgBytesPerSec;

    if (stream_left < read_size) {
        read_size = stream_left;
    }

    HRESULT hr = AllocateBuffer(read_size + min_safety);

    if (FAILED(hr))
    return hr;

    flags |= STREAMED;

    // preload the buffer:
    w = r = 0;
    transfer = new(__FILE__,__LINE__) BYTE[read_size + 1024];

    if (!transfer) {
        hr = E_FAIL;
    }

    else {
        ZeroMemory(transfer, read_size+1024);
        StreamBlock();
    }

    return hr;
}

// +--------------------------------------------------------------------+

HRESULT
SoundD3D::StreamOggFile()
{
    DWORD buf_size    = wfex.nAvgBytesPerSec / 2;
    DWORD safety_zone = buf_size * 2;

    if (stream) {
        delete[] transfer;
        fclose(stream);

        transfer = 0;
        stream   = 0;
    }

    status         = UNINITIALIZED;
    stream_left    = (DWORD) ov_pcm_total(ov_file,-1);
    stream_offset  = 0;

    eos_written    = false;
    eos_latch      = 0;
    min_safety     = safety_zone;
    read_size      = buf_size;

    total_time     = (double) stream_left /
    (double) wfex.nAvgBytesPerSec;

    if (stream_left < read_size) {
        read_size = stream_left;
    }

    HRESULT hr = AllocateBuffer(read_size + min_safety);

    if (FAILED(hr))
    return hr;

    flags |= STREAMED | OGGVORBIS;

    // preload the buffer:
    w = r = 0;
    transfer = new(__FILE__,__LINE__) BYTE[read_size + 1024];

    if (!transfer) {
        hr = E_FAIL;
    }

    else {
        ZeroMemory(transfer, read_size+1024);
        StreamOggBlock();
    }

    return hr;
}

// +--------------------------------------------------------------------+

HRESULT
SoundD3D::Load(DWORD bytes, BYTE* data)
{
    status = UNINITIALIZED;

    HRESULT hr;

    if (!buffer) {
        hr = AllocateBuffer(bytes);
        if (FAILED(hr)) {
            return hr;
        }
    }

    LPVOID   dest1, dest2;
    DWORD    size1, size2;

    hr = buffer->Lock(w, bytes, &dest1, &size1, &dest2, &size2, 0);

    if (hr == DSERR_BUFFERLOST) {
        buffer->Restore();
        hr = buffer->Lock(w, bytes, &dest1, &size1, &dest2, &size2, 0);
    }

    if (SUCCEEDED(hr)) {
        CopyMemory(dest1, data, size1);
        if (dest2) {
            CopyMemory(dest2, data + size1, size2);
        }

        if (flags & STREAMED)
        w = (w + size1 + size2) % (read_size + min_safety);
        else
        w += size1 + size2;

        hr = buffer->Unlock(dest1, size1, dest2, size2);
        if (FAILED(hr)) {
            SoundD3DError("Load: could not unlock buffer", hr);
        }
    }
    else {
        SoundD3DError("Load: could not lock buffer", hr);
    }

    if (SUCCEEDED(hr)) {
        status = READY;
    }

    return hr;
}

// +--------------------------------------------------------------------+

HRESULT
SoundD3D::AllocateBuffer(DWORD bytes)
{
    HRESULT hr = S_OK;

    if (!buffer) {
        dsbd.dwBufferBytes = bytes;

        if (soundcard) {
            hr = soundcard->CreateSoundBuffer(&dsbd, &buffer, NULL);
            
            if (FAILED(hr)) {
                SoundD3DError("AllocateBuffer: could not create buffer", hr);

                Print("   dsbd.dwSize         = %8d\n",  dsbd.dwSize);
                Print("   dsbd.dwFlags        = %08x\n", dsbd.dwFlags);
                Print("   dsbd.dwBufferBytes  = %8d\n",  dsbd.dwBufferBytes);
                Print("   dsbd.lpwfxFormat    = %08x\n", dsbd.lpwfxFormat);

                if (dsbd.lpwfxFormat) {
                    Print("   wfex.wBitsPerSample = %8d\n",  dsbd.lpwfxFormat->wBitsPerSample);
                    Print("   wfex.nChannels      = %8d\n",  dsbd.lpwfxFormat->nChannels);
                    Print("   wfex.nSamplesPerSec = %8d\n",  dsbd.lpwfxFormat->nSamplesPerSec);
                }
            }
        }
        else {
            SoundD3DError("AllocateBuffer: soundcard is null", E_FAIL);
        }
    }

    return hr;
}

// +--------------------------------------------------------------------+

HRESULT
SoundD3D::Play()
{
    if (IsPlaying())  return S_OK;
    if (!buffer)      return E_FAIL;

    HRESULT hr = E_FAIL;

    if (IsDone())
    hr = Rewind();

    if (IsReady()) {
        if (moved)
        Localize();

        if (flags & LOOP || flags & STREAMED)
        hr = buffer->Play(0, 0, DSBPLAY_LOOPING);
        else
        hr = buffer->Play(0, 0, 0);

        if (SUCCEEDED(hr))
        status = PLAYING;
    }

    return hr;
}

// +--------------------------------------------------------------------+

HRESULT
SoundD3D::Rewind()
{
    if (!buffer) return E_FAIL;

    HRESULT hr = S_OK;

    if (IsPlaying())
    hr = Stop();

    if (flags & STREAMED) {
        RewindStream();
        StreamBlock();
    }

    else {
        hr = buffer->SetCurrentPosition(0);
    }

    if (SUCCEEDED(hr)) {
        status = READY;
        looped = 0;
    }

    return hr;
}

// +--------------------------------------------------------------------+

HRESULT
SoundD3D::Pause()
{
    if (status == DONE)
    return S_OK;

    HRESULT hr = Stop();

    if (SUCCEEDED(hr))
    status = READY;

    return hr;
}

// +--------------------------------------------------------------------+

HRESULT
SoundD3D::Stop()
{
    if (!buffer)
    return E_FAIL;

    if (!IsPlaying()) {
        status = DONE;
        return S_OK;
    }

    status = DONE;
    return buffer->Stop();
}

// +--------------------------------------------------------------------+

double
SoundD3D::GetTimeRemaining() const
{
    double time_left = -1;

    if (IsPlaying() || IsReady()) {
        time_left = (double) stream_left /
        (double) wfex.nAvgBytesPerSec;
    }

    return time_left;
}

double
SoundD3D::GetTimeElapsed() const
{
    double time_elapsed = 0;

    if (IsPlaying()) {
        time_elapsed = total_time - GetTimeRemaining();
    }

    return time_elapsed;
}

// +--------------------------------------------------------------------+

void
SoundD3D::SetVolume(long v)
{
    if (v > 0)           v = 0;
    else if (v < -10000) v = -10000;

    volume = v;
    moved  = true;
}

// +--------------------------------------------------------------------+

long
SoundD3D::GetPan() const
{
    long p = 10000;

    if (location.z)     p = (long) fabs(location.x/location.z);
    if (p > 10000)      p = 10000;
    if (location.x < 0) p = -p;

    return p;
}

void
SoundD3D::SetPan(long p)
{
    if (p >  10000) p =  10000;
    if (p < -10000) p = -10000;

    location.x = (float) p;
    location.y = 0.0f;
    location.z = 1.0f;
    moved      = true;
}

// +--------------------------------------------------------------------+

void
SoundD3D::SetLocation(const Vec3& l)
{
    location = l;
    moved    = true;
}

void
SoundD3D::SetVelocity(const Vec3& v)
{
    velocity = v;
    moved    = true;
}

// +--------------------------------------------------------------------+

float
SoundD3D::GetMinDistance() const
{
    return min_dist;
}

void
SoundD3D::SetMinDistance(float f)
{
    min_dist = f;
    moved    = true;
}

// +--------------------------------------------------------------------+

float
SoundD3D::GetMaxDistance() const
{
    return max_dist;
}
void
SoundD3D::SetMaxDistance(float f)
{
    max_dist = f;
    moved    = true;
}


// +--------------------------------------------------------------------+

void
SoundD3DError(const char* msg, HRESULT err)
{
    Print("   SoundD3D: %s. [%s]\n", msg, DSErrStr(err));
}

char*
DSErrStr(HRESULT err)
{
    switch (err) {
    case DS_OK:                           return "DS_OK";

    case DSERR_ALLOCATED:                 return
        "The call failed because resources (such as a priority level) "
        "were already being used by another caller.";

    case DSERR_CONTROLUNAVAIL:            return
        "The control (vol,pan,etc.) requested by the caller is not available.";

    case DSERR_INVALIDPARAM:              return
        "An invalid parameter was passed to the returning function.";

    case DSERR_INVALIDCALL:               return
        "This call is not valid for the current state of this object";

    case DSERR_GENERIC:                   return
        "An undetermined error occured inside the DirectSound subsystem";

    case DSERR_PRIOLEVELNEEDED:           return
        "The caller does not have the priority level required for the function to succeed.";

    case DSERR_OUTOFMEMORY:               return
        "Not enough free memory is available to complete the operation";

    case DSERR_BADFORMAT:                 return
        "The specified WAVE format is not supported";

    case DSERR_UNSUPPORTED:               return
        "The function called is not supported at this time";

    case DSERR_NODRIVER:                  return
        "No sound driver is available for use";

    case DSERR_ALREADYINITIALIZED:        return
        "This object is already initialized";

    case DSERR_NOAGGREGATION:             return
        "This object does not support aggregation";

    case DSERR_BUFFERLOST:                return
        "The buffer memory has been lost, and must be restored.";

    case DSERR_OTHERAPPHASPRIO:           return
        "Another app has a higher priority level, preventing this call from succeeding.";

    case DSERR_UNINITIALIZED:             return
        "This object has not been initialized.";

#ifdef DIRECT_SOUND_3D
    case DSERR_NOINTERFACE:               return
        "The requested COM interface is not available.";
#endif

    default: return "Unknown Error Code";
    }

    return "Internal Error";
}

